const path = require("path");
const fs = require("fs");
// 默认模板
const defaultTemplate = `export async function $METHOD_NAME($ARGS) {
  $BEFORE_CONTENT
  return await fetch($URL, {
    method: $METHOD,
    headers: {
      "Content-Type": $CONTENT_TYPE,
    },
    body: $BODY,
  });
}
`;
// 控制台颜色输出
const colors = {
  red: "\x1b[31m", // 红
  green: "\x1b[32;1m", // 绿
  yellow: "\x1b[33m", // 黄
  blue: "\x1b[34m", // 蓝
  magenta: "\x1b[35m", // 紫
  reset: "\x1b[0m", // 重置
};
// 配置项
const config = {
  maxParams: 5, // 函数最大参数个数
  docComments: true, // 生成文档注释
  functionTitle: true, // 函数标题
  apiContentPath: "", // 路径
  // apiDocUrls: [], // 文档地址可以有多个 apiContentPath 和 apiDocUrls 选其一 apiDocUrls优先级高
  requestBodyVar: "_requestBody", // 参数为对象时请求体的变量名
  queryVar: "_querys", // query 参数为对象时变量名
  formDataVar: "_formData", // formData 参数
  notCreateApi: [], // 哪些api不需要创建
  notCreateDir: [], // 哪些目录下的不需要生成
  // templatePath: "./template.js", // 模板地址 没有使用默认模板
  fileSuffix: ".js", // 生成文件后缀
  // 模板中的占位符
  placeholder: {
    methodName: "$METHOD_NAME", // 函数名
    url: "$URL", // 请求地址
    method: "$METHOD", // 请求方法
    paramsList: "$ARGS", // 参数列表
    contentType: "$CONTENT_TYPE", // 传输数据类型
    body: "$BODY", // 请求体
    beforeContent: "$BEFORE_CONTENT", // 函数体开始
  },
  output: "./api", // 输出目录
  console: {
    dirFileOutput: true, // 文件完成打印信息
    apiOutput: true, // api 完成打印信息
  },
  unifiedPrefix: "/api", // 统一前缀
  maxConcurrent: 4, // 最大并发数
};
// 类型映射
const types = {
  integer: "number",
  number: "number",
  int32: "number",
  int64: "number",
  string: "string",
  boolean: "boolean",
  array: "array",
  null: "null",
  any: "*",
};
// 合并配置
function mergeConfig(originConfig) {
  const args = process.argv.splice(2);
  if (!args.length) return;
  let config = Object.fromEntries(args.map((v) => v.split("=")));
  for (const key in config) {
    let newKey = "";
    let value = config[key].replace(/\\/g, "/");
    if (value === "true" || value === "TRUE") {
      value = true;
    } else if (value === "false" || value === "FALSE") {
      value = false;
    }
    switch (key) {
      case "o":
        newKey = "output";
        break;
      case "args":
        newKey = "maxParams";
        break;
      case "api":
        newKey = "apiContentPath";
        break;
      case "s":
        newKey = "fileSuffix";
        break;
      case "t":
        newKey = "templatePath";
        break;
      case "d":
        newKey = "docComments";
        break;
      case "f":
        newKey = "functionTitle";
        break;
      case "up":
        newKey = "unifiedPrefix";
        break;
    }
    config[newKey] = value;
    if (key !== "config") {
      delete config[key];
    }
  }
  if (config.config) {
    config = require(config.config);
  }
  Object.assign(originConfig, config);
}
/**
 * 读取文件内容
 * @param {*} filePath
 * @returns
 */
async function readFile(filePath) {
  return await fs.promises.readFile(path.resolve(__dirname, filePath), "utf-8");
}
/**
 * 创建api结构
 * @param {*} apis
 * fileName:{
 *   post:[{
 *   api:xxx
 *   query:{xxx},
 *   requestBody:{},
 *   formData:{}
 *  }]
 * }
 */
function createApiStruc(apis, components) {
  const result = {};
  return new Promise((resolve) => {
    // api 就是请求地址
    for (const api in apis) {
      const notApiPrefix = api.replace(`${config.unifiedPrefix}/`, "");
      const slashIndex = notApiPrefix.indexOf("/");
      const fileName = notApiPrefix.substring(
        0,
        slashIndex >= 0 ? slashIndex : notApiPrefix.length
      ); // 作为文件名
      if (!fileName) {
        continue;
      }
      // 初始化
      if (!result[fileName]) {
        result[fileName] = {};
      }
      const curApiInfo = apis[api];
      const addInfo = {
        api,
        description: "",
        query: null,
        formData: null,
        requestBody: null,
      };
      // method 请求方法
      for (const method in curApiInfo) {
        if (!result[fileName][method]) {
          result[fileName][method] = [];
        }

        const curMethodInfo = curApiInfo[method];
        addInfo.description =
          curMethodInfo.summary || curMethodInfo.tags?.join(",") || "方法";
        // 查询参数
        const parameters = curMethodInfo?.parameters?.filter(
          (v) => v.name !== "Authorization"
        );
        const query = handleParams(parameters);
        addInfo.query = query;
        // formData
        if (curMethodInfo.requestBody?.content["multipart/form-data"]) {
          const schema =
            curMethodInfo.requestBody.content["multipart/form-data"].schema;
          const formData = handleFormData(schema);
          addInfo.formData = formData;
        }
        // 请求体参数
        if (curMethodInfo.requestBody?.content["application/json"]) {
          const schema =
            curMethodInfo.requestBody.content["application/json"].schema;
          const ref = schema?.items?.$ref || schema.$ref;
          if (ref) {
            const key = ref.substring(ref.lastIndexOf("/") + 1);
            const propInfo = components.schemas[key];
            const requestBody = handleRequestBody(propInfo);
            addInfo.requestBody = requestBody;
          } else {
            addInfo.requestBody = {
              type: schema.type,
              params: {
                type: transformType(schema?.items?.type || "any"),
                description: "",
                required: true,
              },
              description: "请求体参数",
            };
          }
        }
        result[fileName][method].push(addInfo);
      }
    }
    resolve(result);
  });
}

/**
 * 处理查询参数
 * @param {*} parameters
 */
function handleParams(parameters) {
  if (!parameters?.length) return null;
  const params = {};
  for (const param of parameters) {
    if (param.in === "query") {
      params[param.name] = {
        type: transformType(param.schema.type),
        required: param.required,
        description: param.description || "",
      };
    }
  }
  return {
    type: "query",
    params,
    description: "查询参数",
  };
}

/**
 * 处理请求体参数
 * @param {*} requestBody
 */
function handleRequestBody(requestBodyInfo) {
  if (!requestBodyInfo) return null;
  const { type, properties, description } = requestBodyInfo;
  const params = {};
  for (const key in properties) {
    const prop = properties[key];
    params[key] = {
      type: transformType(prop.type),
      description: prop.description || "",
      required: requestBodyInfo?.required?.includes((v) => v === key) || false,
    };
  }
  return {
    type,
    params,
    description,
  };
}

/**
 * 请求体参数 formData
 */
function handleFormData(formDataInfo) {
  if (!formDataInfo) return null;
  const { type, properties } = formDataInfo;
  const params = {};
  for (const key in properties) {
    const prop = properties[key];
    params[key] = {
      type: transformType(prop.type),
      description: prop.description || "",
      required: true,
    };
  }
  return {
    type,
    params,
    description: "请求体 FormData 类型",
  };
}
/**
 * 类型转换
 */
function transformType(type) {
  return Array.isArray(type)
    ? type.map((v) => types[v]).join("|")
    : types[type];
}

/**
 * 短横线命名法转小驼峰命名法
 * @param {*} name
 */
function transformVar(name) {
  const names = name.split("_");
  names[0] = names[0].toLowerCase();
  return names.join("");
}

/**
 * 获取参数列表
 * @param {*} query
 * @param {*} requestBody
 * @returns
 */
function getParamsList(query, requestBody, isFormData) {
  if (!query && !requestBody) {
    // 都为空
    return "";
  }
  if (typeof isFormData !== "boolean") {
    // 不是布尔值没有请求体参数
    return Object.keys(query?.params || {}).join(",");
  }
  let parameterList = "";
  const queryLen = query ? Object.keys(query.params).length : 0;
  const requestLen = requestBody ? Object.keys(requestBody.params).length : 0;
  if (isFormData) {
    // formData参数
    if (queryLen > config.maxParams) {
      // 合并
      parameterList = `${config.queryVar},${config.formDataVar}`;
    } else {
      parameterList = Object.keys(query?.params || {})
        .concat([config.formDataVar])
        .join(",");
    }
  } else {
    if (requestBody.type === "array") {
      requestBody.params = {
        [config.requestBodyVar]: {
          type: "array",
          description: requestBody.params.description || "数组",
          required: requestBody.params.required,
        },
      };
    }
    if (queryLen + requestLen > config.maxParams) {
      parameterList =
        queryLen > requestLen
          ? Object.keys(requestBody?.params || {}).join(",")
          : Object.keys(query?.params || {}).join(",");
      parameterList +=
        (parameterList.length ? "," : "") +
        (queryLen > requestLen ? config.queryVar : config.requestBodyVar);
    } else {
      parameterList = Object.keys(query?.params || {})
        .concat(Object.keys(requestBody?.params || {}))
        .join(",");
    }
  }
  return parameterList;
}

/**
 * 创建文档注释
 * @param {*} params
 * @param {*} isFormData
 */
function createFnDocComments(params, isFormData) {
  const { query, request, description, queryList, requestList, parameterList } =
    params;
  let paramsType = "";
  if (query && query.params) {
    paramsType += createParamsType(
      query.params,
      queryList ? config.queryVar : ""
    );
  }
  if (request && request.params) {
    paramsType +=
      (paramsType ? "\n" : "") +
      createParamsType(
        request.params,
        requestList
          ? isFormData
            ? config.formDataVar
            : config.requestBodyVar
          : ""
      );
    if (
      typeof isFormData === "boolean" &&
      (parameterList.includes(config.formDataVar) ||
        parameterList.includes(config.requestBodyVar))
    ) {
      paramsType =
        ` * @param {object} ${
          isFormData ? config.formDataVar : config.requestBodyVar
        } 请求体参数\n` + paramsType;
    }
  }

  function createParamsType(params, prefix) {
    return Object.entries(params)
      .map((v) => {
        let template = ` * @param {$TYPE} $VAR $DESCRIPTION`;
        const [p, config] = v;
        template = template.replace("$TYPE", config.type);
        template = template.replace("$VAR", prefix ? `${prefix}.${p}` : `${p}`);
        template = template.replace(
          "$DESCRIPTION",
          (config.required ? "必填 " : "非必填 ") + config.description
        );
        return template;
      })
      .join("\n");
  }

  const docComments = `/**
 * ${description}${(paramsType ? "\n" : "") + paramsType}
 * @returns
 */`;
  return docComments;
}

/**
 * 生成函数体
 * @param {*} functionInfo
 * @returns
 */
function createFnBody(functionInfo, template) {
  const { api, method, isFormData, parameterList, methodName, query, request } =
    functionInfo;
  const contentType = isFormData ? "multipart/form-data" : "application/json";
  let content = template;
  let bodyVar = parameterList.includes(config.requestBodyVar)
    ? config.requestBodyVar
    : parameterList.includes(config.formDataVar)
    ? config.formDataVar
    : null;
  let pre = "";
  if (!parameterList.includes(config.requestBodyVar) && request) {
    // 不包含 config.requestBodyVar 并且 request 存在需要对参数做处理
    bodyVar = "{" + Object.keys(request).join(",") + "}";
  }
  if (!parameterList.includes(config.queryVar) && query) {
    // 没有被合并
    const queryParams = Object.entries(query)
      .map((v) => {
        const [p, config] = v;
        return p + "=" + "${" + p + "}";
        // 不传的情况下需要做区分
        return config.required ? `${p}=${p}` : `${p}=${p}`;
      })
      .join("&");
    pre = "const _queryParams = " + "`" + queryParams + "`;";
  } else if (parameterList.includes(config.queryVar)) {
    // 被合并了 组装查询参数
    pre = `const _queryParams = Object.entries(${config.queryVar}).map((v) => v[0] + '=' + v[1]).join("&");`;
    content.replace(config.placeholder.beforeContent, pre);
  }
  content = content
    .replace(config.placeholder.methodName, methodName) // 函数名
    .replace(config.placeholder.paramsList, parameterList) // 参数列表
    .replace(config.placeholder.beforeContent, query ? pre : "") // 前置操作
    .replace(
      config.placeholder.url,
      query ? "`" + api + "?${_queryParams}" + "`" : `"${api}"`
    ) // url
    .replace(config.placeholder.method, `"${method}"`) // 请求方法
    .replace(config.placeholder.body, bodyVar)
    .replace(config.placeholder.contentType, `"${contentType}"`); // 请求头 contentType
  return content;
}

/**
 * 根据api配置生成文件内容
 * @param {*} apiConfig
 */
async function createFileContent(apiConfig, method, template) {
  // requestBody "application/json" 和 formData "multipart/form-data" 不会共存
  let fileContent = "";
  const { api, query, requestBody, formData, description } = apiConfig;
  const notApiPrefix = api
    .replace(`${config.unifiedPrefix}/`, "")
    .replace("/", "_");
  const request = requestBody ? requestBody : formData;
  const methodName =
    transformVar(notApiPrefix) +
    `${method[0].toUpperCase() + method.substring(1)}`; // 方法名
  const parameterList = getParamsList(
    query,
    request,
    formData ? true : requestBody ? false : ""
  ); // 参数列表
  const functionBody = createFnBody(
    {
      api,
      query: query?.params, // query 需要特殊处理
      request: requestBody?.params,
      method,
      isFormData: formData ? true : requestBody ? false : "",
      methodName,
      parameterList,
    },
    template
  ); // 创建函数体
  fileContent = functionBody;
  // 文档注释
  if (config.docComments) {
    const docComments = createFnDocComments(
      {
        query,
        request,
        description,
        queryList: parameterList.includes(config.queryVar),
        requestList: parameterList.includes(config.requestBodyVar),
        parameterList,
      },
      formData ? true : requestBody ? false : ""
    );
    fileContent = docComments + "\n" + fileContent;
  } else {
    const functionTitle = config.functionTitle ? `// ${description}` : "";
    fileContent = functionTitle + "\n" + fileContent;
  }
  return fileContent;
}

/**
 * 并发请求
 * @param {*} urls
 * @param {*} maxConcurrent
 */
function concurrentRequest(urls, maxConcurrent) {
  return new Promise((resolve) => {
    if (!urls.length) resolve([]);
    maxConcurrent = Math.min(urls.length, maxConcurrent); // 防止最大并发数大于数组长度
    const result = []; // 结果
    let index = 0; // 索引
    let finishCount = 0; // 完成个数
    async function request() {
      const i = index;
      const url = urls[i];
      index++;
      try {
        process.stdout.write(
          `请求 ${colors.magenta}${url}${colors.reset} 中\n`
        );
        const res = await fetch(url).then((v) => v.json());
        result[i] = res;
      } catch (error) {
        result[i] = error;
      } finally {
        finishCount++;
        process.stdout.write(
          `${colors.green}${url}${colors.reset}完成 剩下${colors.green}${
            urls.length - finishCount
          }个${colors.reset} \n`
        );
        if (finishCount === urls.length) {
          resolve(result);
        } else if (index < urls.length) {
          request();
        }
      }
    }

    for (let i = 0; i < maxConcurrent; i++) {
      request();
    }
  });
}

// 主方法
async function main() {
  if (!config.apiContentPath && !config.apiDocUrls?.length) {
    // 不存在 api 文件
    process.stdout.write(
      `${colors.red}请配置 apifox导出文件路径/接口文档路径${colors.reset} 或 不存在该路径`
    );
    return;
  }
  let data = {};
  if (config.apiDocUrls?.length) {
    const result = await concurrentRequest(
      config.apiDocUrls,
      config.maxConcurrent
    );
    for (const item of result) {
      if (Object.keys(data).length) {
        // 目前只用到了 paths components
        Object.assign(data.paths, item.paths);
        Object.assign(data.components, item.components);
      } else {
        data = item;
      }
    }
  } else if (config.apiContentPath) {
    const fileContent = await readFile(config.apiContentPath);
    data = JSON.parse(fileContent);
  }
  const template = config.templatePath
    ? await readFile(config.templatePath)
    : defaultTemplate;
  // 处理好的api结构
  const apiStruc = await createApiStruc(data.paths, data.components);
  const dirPath = path.resolve(__dirname, config.output);
  let totalFile = 0;
  let totalApi = 0;
  if (fs.existsSync(dirPath)) {
    // 存在删除文件
    await fs.promises.rm(dirPath, { recursive: true });
  }
  await fs.promises.mkdir(dirPath).catch((e) => {
    process.stdout.write(`${colors.red}文件夹创建失败${colors.reset}`);
    throw e;
  });
  for (const key in apiStruc) {
    if (config.notCreateDir.includes(key)) continue;
    // 每个类别
    const fileName = key + config.fileSuffix;
    const filePath = path.resolve(dirPath, fileName);
    let fileContent = "";
    let curApi = 0;
    for (const m in apiStruc[key]) {
      // 每种请求方法
      for (const apiConfig of apiStruc[key][m]) {
        if (config.notCreateApi.includes(apiConfig.api)) continue;
        const methodContent = await createFileContent(apiConfig, m, template);
        fileContent += methodContent + "\n";
        totalApi++;
        curApi++;
        if (config.console.apiOutput) {
          process.stdout.write(
            `${apiConfig.api} ${colors.blue}完成${colors.reset}\n`
          );
        }
      }
    }
    await fs.promises.writeFile(filePath, fileContent);
    totalFile++;
    if (config.console.dirFileOutput) {
      process.stdout.write(
        `${colors.yellow}${fileName}完成${colors.reset} 共${colors.green}${curApi}${colors.reset}个\n`
      );
      console.log("------------------------------");
    }
  }
  process.stdout.write(
    `自动创建完成 共生成 ${colors.green}${totalFile}${colors.reset}个文件 ${colors.green}${totalApi}${colors.reset}Api方法`
  );
}
// 合并配置项
mergeConfig(config);
// 启动
main();
